/*
   Copyright 2009 Abdulla Abdurakhmanov (abdulla@latestbit.com)
   Original sources are available at www.latestbit.com

   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.
*/
package commons

import java.util.Properties;
import org.latestbit.smartobjects.smartidlc.*
import org.latestbit.smartobjects.smartidlc.model.*
import groovy.text.Template
import groovy.text.SimpleTemplateEngine
import groovy.text.GStringTemplateEngine
import org.latestbit.smartobjects.smartidlc.model.IDLInterfaceOperationParam

/**
 * @author abdulla-a
 *
 */
public abstract class TargetLangModule implements ITargetLangModule {	   
	String rootDir;
	String templatesDir;
	String moduleName;	
	String outputDir = "./";
	CompilerOptions compilerOptions;
	IMessageReporter reporter;
	ICodeBeautifier beautifier;
	Properties targetArgs;
	def beautifierFileSet = [] as Set;
	def generatedFileSet = [] as List;
	boolean serverGenerationMode = false; 
	
	def targetNamespace;
	def loadedNamespaces;	   
	
	def currentBindings = null;
	def utils = new Utils();
	
	def isServerGenerationMode() {
		return serverGenerationMode;
	}
	
	def setServerGenerationMode(boolean value) {
		this.serverGenerationMode = value;
	}
	
	def setModuleName(moduleName) {
		this.moduleName = moduleName;
	}
	
	def checkDirSeparator(path) {
		if (!path.trim().endsWith(File.separator)) {
			return path+File.separator;
		}
		else
		return path;
	}
	
	def void setModulesRootDir(String directory) {
		this.rootDir = 
		checkDirSeparator(directory) + 
		"targets" + File.separator + 
		this.moduleName + File.separator;			   
		this.templatesDir = this.rootDir + "templates" + File.separator;
	}
	
	def void setOutputDir(String directory) {
		this.outputDir = directory.trim();
		this.outputDir = checkDirSeparator(this.outputDir);		   
	}
	
	def void setCompilerOptions ( CompilerOptions options ) {
		this.compilerOptions = options;
	}
	
	def void setMessageReporter ( IMessageReporter reporter ) {
		this.reporter = reporter;
	}
	
	def void setCodeBeautifier(ICodeBeautifier beautifier) {		
		this.beautifier = beautifier;
	}
	
	def void setTargetArgs(Properties args) {
		this.targetArgs = args;
	}
	
	def templatesCache = [:] as Map;
	GStringTemplateEngine templateEngine = new GStringTemplateEngine();	  
	
	def getTemplate(fileTemplate) {
		def template = null;
		if(!templatesCache.containsKey(fileTemplate)) {
			template = templateEngine.createTemplate(fileTemplate);
			templatesCache.put(fileTemplate,template);			    
		}
		else {
			template = templatesCache.get(fileTemplate);
		}		   
		return template;
	}
	
	def processTemplate(templateName, binding, outputFileName = null) {		   
		def fileTemplate = new File ( this.templatesDir + templateName + ".tmpl" );
		try {
			def template = getTemplate(fileTemplate);
			
			binding+= ["module" : this];
			binding+= ["utils" : utils];
			binding+= ["outputFileName" : outputFileName];
			String outputFileNameWE="";
			def parts = outputFileName.split("\\.");
			for(int i=0; i < parts.size()-1; i++) {
				outputFileNameWE+=parts[i];
			}
			binding+= ["outputFileNameWE" : outputFileNameWE];
			currentBindings = binding;
			if ( outputFileName ) {
				File outputFile = new File ( 
						this.outputDir + outputFileName 
				);
				
				if ( !outputFile.exists() )
					outputFile.createNewFile();
				
				def outputFileWriter = outputFile.newWriter("UTF-8");
				
				template.make(binding).writeTo( outputFileWriter );
				outputFileWriter.flush();
				outputFileWriter.close();
				generatedFileSet.add(outputFile.getName());
				
				if(this.beautifier!=null) {
					this.beautifierFileSet.add (outputFile.getAbsolutePath());					
					if(this.beautifierFileSet.size() > 15) {
						runBeautifier();
					}
				}
			}
			else
				return template.toString();
		}
		catch(Exception ex) {
			throw new Exception("Error processing template '"+fileTemplate+"'",ex);
		}
	}
	
	
	def processSubTemplate(templateName, binding = null) {		   
		def fileTemplate = new File ( this.templatesDir + templateName + ".tmpl" );
		try {
			def template = getTemplate(fileTemplate);
			
			def allBindings = currentBindings;
			if(binding)
			allBindings+= binding;
			return template.make(allBindings).toString();
		}
		catch(Exception ex) {
			throw new Exception("Error processing template '"+fileTemplate+"'",ex);
		}			   
	}
	
	def toEncodePrimitiveTypeFunctionName(IDLPrimitiveType primitiveType) {
		switch ( primitiveType.typeId ) {
			case IDLPrimitiveType.TypeId.BOOLEAN:
			return "Boolean";
			case IDLPrimitiveType.TypeId.DATETIME:
			return "DateTime";				
			case IDLPrimitiveType.TypeId.BYTE:
			return "Byte";
			break;				
			case IDLPrimitiveType.TypeId.UNSIGNED_BYTE:
			return "UByte";
			break;
			case IDLPrimitiveType.TypeId.CHAR:
			return "Char";
			break;				
			case IDLPrimitiveType.TypeId.WCHAR:
			return "WChar";
			break;
			case IDLPrimitiveType.TypeId.DOUBLE:
			return "Double";
			break;
			case IDLPrimitiveType.TypeId.FLOAT:
			return "Float";
			break;
			case IDLPrimitiveType.TypeId.INT16:
			return "Int16";
			break;
			case IDLPrimitiveType.TypeId.UINT16:
			return "UInt16";
			break;
			case IDLPrimitiveType.TypeId.INT32:
			return "Int32";
			break;
			case IDLPrimitiveType.TypeId.UINT32:
			return "UInt32";
			break;
			case IDLPrimitiveType.TypeId.INT64:
			return "Int64";
			break;
			case IDLPrimitiveType.TypeId.UINT64:
			return "UInt64";
			break;
			case IDLPrimitiveType.TypeId.STRING:
			return "String";
			break;				
			case IDLPrimitiveType.TypeId.WSTRING:				
			return "WString";
			break;
			case IDLPrimitiveType.TypeId.BIGINTEGER:				
			return "BigInteger";
			break;
			case IDLPrimitiveType.TypeId.BIGDECIMAL:				
			return "BigDecimal";
			break;												
		}
	}
	
	def generateSerializer(IDLPrimitiveType idlPrimitiveType, String fieldName, boolean deserializer = false) {
		IDLDataType idlType = new IDLDataType();
		idlType.primitiveType = idlPrimitiveType;
		idlType.typeId = IDLDataType.DataTypeId.PRIMITIVE;
		return generateSerializer(idlType,fieldName, deserializer);
	}
	
	def generateSerializer(IDLDataType idlType, String fieldName, boolean deserializer = false) {		
		def subBindings = [ "idlType" : idlType ];
		subBindings+= [ "fieldName" : fieldName ];
		def postfixName = deserializer ?  "deserializer" : "serializer";
		
		if( idlType.required && isNullable(idlType) )
			return processSubTemplate("types/serializers/required_field_${postfixName}", subBindings);
		if(idlType.isEncrypted()) {
			return processSubTemplate("types/serializers/encrypted_field_${postfixName}", subBindings);
		}
		if(idlType.typeId.equals(IDLDataType.DataTypeId.QUALIFIED_NAME )) {
			return processSubTemplate("types/serializers/object_type_${postfixName}", subBindings);
		}
		if(idlType.typeId.equals(IDLDataType.DataTypeId.PRIMITIVE )) {
			return processSubTemplate("types/serializers/primitive_type_${postfixName}", subBindings);
		}
		if(idlType.typeId.equals(IDLDataType.DataTypeId.COMPLEX )) {
			
			switch ( idlType.complexType.complexTypeId ) {			
				case IDLComplexType.ComplexTypeId.ARRAY:
				if(idlType.complexType.complexItemDataType.typeId.equals(IDLDataType.DataTypeId.PRIMITIVE ) ) {
					return processSubTemplate("types/serializers/array_of_primitive_type_${postfixName}", subBindings);
				}
				else {
					if(idlType.complexType.complexItemDataType.typeId==IDLDataType.DataTypeId.COMPLEX 
					&& idlType.complexType.complexItemDataType.complexType.complexTypeId == IDLComplexType.ComplexTypeId.ARRAY)
					return processSubTemplate("types/serializers/array_of_array_type_${postfixName}", subBindings);
					else
					return processSubTemplate("types/serializers/array_of_complex_type_${postfixName}", subBindings);
				}
				break;
				case IDLComplexType.ComplexTypeId.LIST:				
				return processSubTemplate("types/serializers/list_of_type_${postfixName}", subBindings);
				break;
				case IDLComplexType.ComplexTypeId.MAP:
				return processSubTemplate("types/serializers/map_of_type_${postfixName}", subBindings);
				break;
			}
		}	
	}
	
	def generatePrimitiveTypeValidator ( IDLPrimitiveType idlPrimitiveType, String fieldName ) {
		def subBindings = [ "idlPrimitiveType" : idlPrimitiveType ];
		subBindings+= [ "fieldName" : fieldName ];
		String result = "";
		switch ( idlPrimitiveType.typeId ) {
			case IDLPrimitiveType.TypeId.BYTE:
			case IDLPrimitiveType.TypeId.UNSIGNED_BYTE:
			case IDLPrimitiveType.TypeId.DOUBLE:
			case IDLPrimitiveType.TypeId.FLOAT:
			case IDLPrimitiveType.TypeId.INT16:
			case IDLPrimitiveType.TypeId.UINT16:
			case IDLPrimitiveType.TypeId.INT32:
			case IDLPrimitiveType.TypeId.UINT32:
			case IDLPrimitiveType.TypeId.INT64:
			case IDLPrimitiveType.TypeId.UINT64:
				if ( idlPrimitiveType.rangeConstraints!=null )
					result+=processSubTemplate("types/serializers/number_type_validator", subBindings);
			break;
			case IDLPrimitiveType.TypeId.BIGINTEGER:
				if ( idlPrimitiveType.rangeConstraints!=null )
					result+=processSubTemplate("types/serializers/biginteger_type_validator", subBindings);
			break;
			case IDLPrimitiveType.TypeId.BIGDECIMAL:
				if ( idlPrimitiveType.rangeConstraints!=null )
					result+=processSubTemplate("types/serializers/bigdecimal_type_validator", subBindings);
			break;			
			case IDLPrimitiveType.TypeId.STRING:
			case IDLPrimitiveType.TypeId.WSTRING:
			
			if ( idlPrimitiveType.stringSizeConstraint!=null )
			result+=processSubTemplate("types/serializers/string_type_size_validator", subBindings);
			if ( idlPrimitiveType.stringMaskConstraints!=null )
			result+=processSubTemplate("types/serializers/string_type_masks_validator", subBindings);
			break;
		}
		return result;
	}
	
	def generateResultSerializer ( IDLInterfaceOperation oper, String fieldName, boolean deserializer = false  ) {
		if ( !oper.interfaceReferenceResult ) {
			if( oper.resultSessionId && !this.serverGenerationMode) {
				def subBindings = [ "idlType" : oper.resultType ];
				subBindings+= [ "fieldName" : fieldName ];
				def postfixName = deserializer ?  "deserializer" : "serializer";
				return processSubTemplate("types/serializers/sessionid_result_${postfixName}", subBindings);					
			}
			else
			return generateSerializer( oper.resultType, fieldName, deserializer );
		}
		else {
			def subBindings = [ "idlType" : oper.resultType ];
			subBindings+= [ "fieldName" : fieldName ];
			def postfixName = deserializer ?  "deserializer" : "serializer";
			return processSubTemplate("types/serializers/skeleton_result_${postfixName}", subBindings);
		}			
	}
	
	def generateParamSerializer ( IDLInterfaceOperationParam param, String fieldName, boolean deserializer = false  ) {
		if ( !param.isInterfaceReference()  ) {
			if(param.sessionId && !this.serverGenerationMode) {
				def subBindings = [ "idlType" : param.type ];
				subBindings+= [ "fieldName" : fieldName ];
				def postfixName = deserializer ?  "deserializer" : "serializer";					
				return processSubTemplate("types/serializers/sessionid_param_${postfixName}", subBindings);
			}
			else				
			return generateSerializer( param.type, fieldName, deserializer );
		}
		else {
			def subBindings = [ "idlType" : param.type ];
			subBindings+= [ "fieldName" : fieldName ];
			def postfixName = deserializer ?  "deserializer" : "serializer";
			return processSubTemplate("types/serializers/skeleton_param_${postfixName}", subBindings);
		}			
	}
	
	def void processNamespace (IDLNamespace targetNamespace, Map<String, IDLNamespace> loadedNamespaces) {
		this.targetNamespace = targetNamespace;
		this.loadedNamespaces = loadedNamespaces;
		
		def saveOutputDir = this.outputDir;
		
		parseTargetArgs();		
		generateDirectories();		
		reporter.onVerboseMessage("--> Generating typedefs");
		generateTypeDefs ( );
		reporter.onVerboseMessage("--> Generating structures");
		generateStructures ( );
		reporter.onVerboseMessage("--> Generating choices");
		generateChoices ( );
		reporter.onVerboseMessage("--> Generating enums");
		generateEnums ( );
		reporter.onVerboseMessage("--> Generating exceptions");
		generateExceptions ( );		
		reporter.onVerboseMessage("--> Generating interfaces");
		generateInterfaces ( );
		runBeautifier();
		generateTargetNamespace();
		this.outputDir = saveOutputDir;		
	}
	
	def parseTargetArgs() {}
	
	def generateTargetNamespace() {}
	
	def generateTypeDefs ( ) {
		targetNamespace.typedefs.each {
			generateTypeDef ( it );
		}
	}
	
	def generateStructures ( ) {
		targetNamespace.structures.each {
			generateStructure ( it );
		}
	}	
	
	def generateChoices ( ) {
		targetNamespace.choices.each {
			generateChoice ( it );
		}
	}
	
	def generateEnums ( ) {
		targetNamespace.enums.each {
			generateEnum ( it );
		}
	}
	
	def generateInterfaces ( ) {
		targetNamespace.interfaces.each {
			generateInterface ( it );
		}
	}
	
	def generateExceptions ( ) {
		targetNamespace.exceptions.each {
			generateException ( it );
		}
	}
	
	def boolean findAllParentsWithChild ( target, parents ) {
		if ( parents instanceof IDLTypeDef ) {
			return findAllParentsWithChild ( target, parent.refDataType );
		}
		else
		if ( parents instanceof IDLStructure || parents instanceof IDLChoice ) {
			return findAllParentsWithChild ( target, parents.parents );
		}
		else
		if ( parents instanceof IDLDataType ) {
			return findAllParentsWithChild ( target, parents.userObject );
		}
		else
		if ( parents instanceof IDLException ) {
			if(parents.parent == null)
			return false;
			else
			if ( parents.parent == target )
			return true;
			else
			return findAllParentsWithChild( target, parents.parent );
		}
		
		def result = false;
		parents.each { parent ->
			if ( parent instanceof IDLTypeDef ) {
				if ( parent.refDataType == target ) {
					result = true;
					return result;
				}
				else {
					if( findAllParentsWithChild ( target, parent.refDataType ) ) {
						result = true;
						return result;
					}						
				}
			}
			else {
				if ( parent == target ) {
					result = true;
					return result;
				}
				else {
					if( findAllParentsWithChild ( target, parent.parents) ) {
						result = true;
						return result;
					}
				}
			}
		}
		return result;
	}
	
	def findAllChilds( IDLDataType idlType ) {
		def result = [] as Set;
		if ( idlType.typeId == IDLDataType.DataTypeId.QUALIFIED_NAME ) {
			
			loadedNamespaces.each { namespace -> 
				result += findAllChildsInNamespace ( namespace.value, idlType);		
			}
		}
		return result;
	}
	
	def findAllChildsInTNS( IDLDataType idlType ) {
		if ( idlType.typeId == IDLDataType.DataTypeId.QUALIFIED_NAME ) {
			return findAllChildsInNamespace ( targetNamespace, idlType);
		}
		else
			return [] as Set;
	}
	
	def findAllChildsInNamespace(IDLNamespace namespace, IDLDataType idlType) {
		def result = [] as Set;
		
		namespace.structures.each { structure ->
			if ( findAllParentsWithChild ( idlType.userObject , structure.parents ))
				result+=structure;
		}
		
		namespace.exceptions.each { exception ->
			if ( findAllParentsWithChild ( idlType.userObject , exception.parent ))
				result+=exception;
		}
		
		namespace.choices.each { choice ->
			if ( findAllParentsWithChild ( idlType.userObject , choice.parents ))
				result+=choice;
		}
		
		return result;
	}
	
	def findOperationIdIntersectionInParents (IDLInterface intf, boolean onlyFirst = false) {
		def result = [] as List;
				
		def nameSet = [] as Set;
		intf.operations.each { operation -> 
			nameSet.add ( operation.name );
		}
		
		intf.allOperations.each { operation ->
			if(operation.declaredInInterface!=intf) {
				if(nameSet.contains(operation.name)) {
					result += operation;
					if(onlyFirst)
						nameSet.remove(operation.name);
				}
			}
		}
		return result;
	}
	
	def runBeautifier() {
		if(this.beautifier!=null) {
			this.beautifier.beautify ( this.beautifierFileSet, this.getTargetLanguageName());
			this.beautifierFileSet.clear();
		}
	}
	
	def getGeneratedFileSet() {
		return this.generatedFileSet;
	}
}
